/**
 * Copyright 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @fileoverview This file implements a gadget sandbox. Simply put the
 * sandbox sets up an in memory container and provides a framework for
 * gadgets to operate within it. It serves two purposes.
 * (a) Demonstrate the concept of a container using a trivial example.
 * (b) Easily test gadgets with arbitrary initial state.
 *
 * The gadget developer can specify the gadget definition file and the
 * initial state using XML files and the sandbox will load the gadget
 * with that state. At any point, a sanpshot of the container state
 * can be dumped back to an XML file.
 */


/**
 * The gadget sandbox.
 *
 * @constructor
 */
var Sandbox = function() {
  /**
   * {String} gadgetUrl URL to the gadget definition file.
   * @private
   */
  this.gadgetUrl = null;

  /**
   * {String} stateUrl URL to the initial state of the container.
   * @private
   */
  this.stateUrl = null;

  /**
   * {opensocial.Container} mockContainer is a singleton instance of the in
   * memory container.
   * @private
   */
  this.mockContainer = null;

  /**
   * {String} gadgetContentDivId The id of the div for the gadget content
   * JQuery will not evaluate correctly if there is a '.' in the middle of the
   * div id. Hence the '_'.
   * @private
   */
  this.gadgetContentDivId = 'Sandbox_gadgetContent';
  this.gadgetContentDiv = document.getElementById(this.gadgetContentDivId);
};


/**
 * Arbitrary value for setting when the gadget url cookie will expire
 */
Sandbox.COOKIE_EXPIRE_DAYS = 365;


/**
 * Initialize the page by loading previously set values, primarily
 * for convenience.
 */
Sandbox.prototype.init = function() {
  // Read the cookies and load the page contents
  this.readCookie();
  this.refreshState();
};


/**
 * Read the cookie to retrieve the gadget defintion and initial state.
 * This is handy so the gadget developer need not enter these URLs
 * every time.
 * @private
 */
Sandbox.prototype.readCookie = function() {
  var cookieValue = unescape(Sandbox.extractField(document.cookie, 'Sandbox'));
  this.gadgetUrl = unescape(Sandbox.extractField(cookieValue, 'gadgetUrl'));
  this.stateUrl = unescape(Sandbox.extractField(cookieValue, 'stateUrl'));

  if (this.gadgetUrl) {
    document.getElementById("Sandbox.gadgetUrl").value = this.gadgetUrl;
  }
  if (this.stateUrl) {
    document.getElementById("Sandbox.stateUrl").value = this.stateUrl;
  }
};


/**
 * Store the gadget definition and initial state URLs in a cookie.
 * @private
 */
Sandbox.prototype.setCookie = function() {
  var cookieValue = 'gadgetUrl=' + escape(this.gadgetUrl) + ';'
      + 'stateUrl=' + escape(this.stateUrl) + ';';

  var expiryDate = new Date();
  expiryDate.setDate(expiryDate.getDate() + Sandbox.COOKIE_EXPIRE_DAYS);
  document.cookie = 'Sandbox=' + escape(cookieValue)
      + ";expires=" + expiryDate.toGMTString();
};


/**
 * Static helper function to read fields from cookies.
 * @private
 */
Sandbox.extractField = function(rawText, fieldName) {
  var fieldStart = rawText.indexOf(fieldName + "=");
  if (fieldStart < 0) {
    return "";
  }

  fieldStart = fieldStart + fieldName.length + 1;
  var fieldEnd = rawText.indexOf(";", fieldStart);
  if (fieldEnd < 0) {
    fieldEnd = rawText.length;
  }

  return rawText.substring(fieldStart, fieldEnd);
};


/**
 * Load the gadget from the specified URL.
 */
Sandbox.prototype.addGadget = function() {
  // Get the gadget url field and set the user's cookie
  this.gadgetUrl = document.getElementById("Sandbox.gadgetUrl").value;
  this.setCookie();

  document.getElementById("Sandbox.message").innerHTML =
      'Gadget URL has been set to ' + this.gadgetUrl + '.<br>';

  this.refreshGadget();
};


/**
 * Load the contianer state from the specified URL.
 */
Sandbox.prototype.setState = function() {
  // Get the state field and set the user's cookie
  this.stateUrl = document.getElementById("Sandbox.stateUrl").value;
  this.setCookie();

  document.getElementById("Sandbox.message").innerHTML =
      'State URL has been set to ' + this.stateUrl + '.<br>';

  // Force the contianer state to be fetched and reloaded
  this.refreshState();
};


/**
 * Make an ajax request to fetch the XML file.
 */
Sandbox.prototype.refreshState = function() {
  // Make sure we have a valid state URL
  if (this.stateUrl.length <= 0) {
    this.gadgetContentDiv.innerHTML = 'Please enter a container state url. ';
    return;
  }

  // Fetch the container state XML
  this.gadgetContentDiv.innerHTML =
      'Loading container state from ' + this.stateUrl + '<br>' +
      'If this message persists make sure your URLs are in the same ' +
      'domain as this page.';

  var me = this;
  $(function() {
    $.ajax({type: "GET", url: me.stateUrl, dataType: "xml", timeout: 5000,
      error: function() {
        me.gadgetContentDiv.innerHTML
            = 'Cannot fetch container state from ' + me.stateUrl;
      },
      success: function(xmlState) {
        me.onLoadState(xmlState);
      }
    }); // ajax request
  }); // jquery function call
};


/**
 * This function will get called after successful download of the
 * container state XML.
 * @private
 */
Sandbox.prototype.onLoadState = function(xmlState) {
  // Get the high level container node
  var containerNode = $(xmlState).find('container')[0];
  if (!containerNode) {
    this.gadgetContentDiv.innerHTML
        = 'Invalid container state XML at ' + this.stateUrl;
    return;
  }

  // Get the viewer node
  var viewer;
  var viewerNode = $(containerNode).find('viewer')[0];
  if (viewerNode) {
    viewer = this.loadPerson($(viewerNode).find('person')[0], true);
  }

  // Get the owner node
  var owner;
  var ownerNode = $(containerNode).find('owner')[0];
  if (ownerNode) {
    owner = this.loadPerson($(ownerNode).find('person')[0], false, true);
  }

  // If the id of the owner is the same as the viewer, then set the viewer
  // as the primary source of truth
  if (!owner || (viewer && owner.getId() == viewer.getId())) {
    owner = viewer;
    owner.isViewer = true;
    owner.isOwner = true;
  }

  // Build the friends list
  var me = this;
  var viewerFriends = new Array();
  var friendsNode = $(containerNode).find('viewerFriends')[0];
  $(friendsNode).find('person').each(function() {
    viewerFriends.push(me.loadPerson($(this)));
  });
  var ownerFriends = new Array();
  friendsNode = $(containerNode).find('ownerFriends')[0];
  $(friendsNode).find('person').each(function() {
    ownerFriends.push(me.loadPerson($(this)));
  });

  // Build the App data
  var globalAppData = {};
  var globalDataNode = $(containerNode).find('globalAppData')[0];
  if (globalDataNode) {
    $(globalDataNode).find('data').each(function() {
      globalAppData[$(this).attr('field')] = $(this).text();
    });
  }

  var instanceAppData = {};
  var instanceDataNode = $(containerNode).find('instanceAppData')[0];
  if (instanceDataNode) {
    $(instanceDataNode).find('data').each(function() {
      instanceAppData[$(this).attr('field')] = $(this).text();
    });
  }

  var personAppData = {};
  var personDataNode = $(containerNode).find('personAppData')[0];
  if (personDataNode) {
    $(personDataNode).find('data').each(function() {
      if (personAppData[$(this).attr('person')] == null) {
        personAppData[$(this).attr('person')] = {};
      }
      personAppData[$(this).attr('person')][$(this).attr('field')]
          = $(this).text();
    });
  }

  // Build the activities list
  var appIdNode = $(containerNode).find('appId')[0];
  var appId = appIdNode ? $(appIdNode).text() : 'sampleContainerAppId';

  var activities = {};
  var activitiesNode = $(containerNode).find('activities')[0];
  $(activitiesNode).find('stream').each(function() {
    var stream = new opensocial.Stream($(this).attr('title'),
        {'url' : $(this).attr('url'),
         'userId' : $(this).attr('userId'),
         'appId' : appId,
         'sourceUrl' : $(this).attr('sourceUrl'),
         'faviconUrl' : $(this).attr('faviconUrl')});
    activities[stream.getField('userId')] = [];

    $(this).find('activity').each(function() {
      var mediaItems = [];
      $(this).find('mediaItem').each(function() {
        mediaItems.push(new opensocial.Activity.MediaItem(
            $(this).attr('mimeType'),
            $(this).attr('url'),
            {'type' : $(this).attr('type')}));
      });
      activities[stream.getField('userId')].push(new opensocial.Activity(stream,
          $(this).attr('title'),
          {'id' : $(this).attr('id'), 'externalId' : $(this).attr('externalId'),
          'summary' : $(this).attr('summary'), 'body' : $(this).attr('body'),
          'url' : $(this).attr('url'), 'folder' : $(this).attr('folder'),
          'postedTime' : $(this).attr('postedTime'),
          'mediaItems' : mediaItems}));
    });
  });

  // Initialize the mock container with the state that has been read
  this.mockContainer = new opensocial.RailsContainer(viewer, owner,
      new opensocial.Collection(viewerFriends),
      new opensocial.Collection(ownerFriends), globalAppData,
      instanceAppData, personAppData, activities, appId);

  opensocial.Container.setContainer(this.mockContainer);

  // Now load the gadget
  this.refreshGadget();
};


/**
 * load a person related info from the XML node. Person could be
 * viewer, owner or friend. Return value is the person object.
 * @private
 */
Sandbox.prototype.loadPerson = function(xmlNode, isViewer, isOwner) {
  var fields = {
    'id' : $(xmlNode).attr(opensocial.Person.Field.ID),
    'name' : $(xmlNode).attr(opensocial.Person.Field.NAME),
    'thumbnailUrl' : $(xmlNode).attr(opensocial.Person.Field.THUMBNAIL_URL),
    'profileUrl' : $(xmlNode).attr(opensocial.Person.Field.PROFILE_URL)};
  return new opensocial.Person(fields, isViewer, isOwner);
};


/**
 * Dumps the current state of the container in XML.
 */
Sandbox.prototype.dumpState = function() {
  var xmlText = '<container>\n';

  xmlText += '  <viewer>\n';
  xmlText += this.dumpPerson(this.mockContainer.viewer);
  xmlText += '  </viewer>\n';

  xmlText += '  <owner>\n';
  xmlText += this.dumpPerson(this.mockContainer.owner);
  xmlText += '  </owner>\n';

  var me = this;
  xmlText += '  <viewerFriends>\n';
  this.mockContainer.viewerFriends.each(function(friend) {
    xmlText += me.dumpPerson(friend);
  });
  xmlText += '  </viewerFriends>\n';

  xmlText += '  <ownerFriends>\n';
  this.mockContainer.ownerFriends.each(function(friend) {
    xmlText += me.dumpPerson(friend);
  });
  xmlText += '  </ownerFriends>\n';

  // Dump App Data
  xmlText += '  <globalAppData>\n';
  for (var field in this.mockContainer.globalAppData) {
     xmlText += '    <data field="' + field + '">';
     xmlText += this.mockContainer.globalAppData[field];
     xmlText += '</data>\n';
  }
  xmlText += '  </globalAppData>\n';

  xmlText += '  <instanceAppData>\n';
  for (var field in this.mockContainer.instanceAppData) {
     xmlText += '    <data field="' + field + '">';
     xmlText += this.mockContainer.instanceAppData[field];
     xmlText += '</data>\n';
  }
  xmlText += '  </instanceAppData>\n';

  xmlText += '  <personAppData>\n';
  for (var person in this.mockContainer.personAppData) {
    for (var field in this.mockContainer.personAppData[person]) {
       xmlText += '    <data person="' + person + '" ';
       xmlText += 'field="' + field + '">';
       xmlText += this.mockContainer.personAppData[person][field];
       xmlText += '</data>\n';
    }
  }
  xmlText += '  </personAppData>\n';

  // Dump the activities. Since only 1 stream is supported, use the first one
  xmlText += '  <activities>\n';
  var streamWritten = false;
  for (var id in this.mockContainer.activities) {
    var activity = this.mockContainer.activities[id];
    if (streamWritten == false) {
      var stream = activity.getField('stream');
      if (!stream) {
        continue;
      }
      xmlText += '    <stream';
      for (var field in stream.fields_) {
        var value = stream.getField(field);
        if (value == null) {
          continue;
        }
        xmlText += ' ' + field + '="' + value + '"';
      }
      xmlText += '>\n';
      streamWritten = true;
    }

    xmlText += '      <activity';
    for (var field in activity.fields_) {
      var value = activity.getField(field);
      if (value == null || field == 'mediaItems' || field == 'stream') {
        continue;
      }
      xmlText += ' ' + field + '="' + value + '"';
    }
    xmlText += '>';
    var mediaItems = activity.mediaItems;
    for (var i = 0; mediaItems && i < mediaItems.length; i++) {
      var mediaItem = mediaItem[i];
      xmlText += '        <mediaItem ';
      if (mediaItem.mimeType) {
        xmlText += ' mimeType="' + mediaItem.mimeType + '"';
      }
      if (mediaItem.url) {
        xmlText += ' url="' + mediaItem.url + '"';
      }
      if (mediaItem.opt_params && mediaItem.opt_params.type) {
        xmlText += ' type="' + mediaItem.opt_params.type + '"';
      }
      xmlText += '/>\n';
    }
    xmlText += '</activity>\n';
  }
  if (streamWritten) {
    xmlText += '    </stream>\n';
  }
  xmlText += '  </activities>\n';

  xmlText += '</container>';
  document.getElementById('Sandbox.currentState').innerHTML =
      '<hr>Current State:<br>' + '<textarea rows="15" cols="120">'
      + xmlText + '</textarea>';
};


/**
 * @private
 */
Sandbox.prototype.dumpPersonField = function(personObj, name) {
  var field = personObj.getField(name);
  if (field) {
    return ' ' + name + '="' + field + '"';
  }
  return '';
};



/**
 * Dump the state of a person object.
 * @private
 */
Sandbox.prototype.dumpPerson = function(personObj) {
  var xmlText = '    <person';
  xmlText += this.dumpPersonField(personObj, opensocial.Person.Field.ID);
  xmlText += this.dumpPersonField(personObj, opensocial.Person.Field.NAME);
  xmlText += this.dumpPersonField(personObj,
      opensocial.Person.Field.THUMBNAIL_URL);
  xmlText += this.dumpPersonField(personObj,
      opensocial.Person.Field.PROFILE_URL);
  xmlText += '></person>\n';
  return xmlText;
};


/**
 * Make an ajax request to fetch the gadget definition.
 */
Sandbox.prototype.refreshGadget = function() {
  // If there is no URL specified, ask user to please enter one
  if (this.gadgetUrl.length <= 0) {
    this.gadgetContentDiv.innerHTML = 'Please enter a gadget location url.';
    return;
  }

  this.gadgetContentDiv.innerHTML
      = 'Fetching gadget from ' + this.gadgetUrl + ' ...' + '<br>' +
      'If this message persists make sure your URLs are in the same ' +
      'domain as this page.';

  // Fetch the gadget content from the site
  var me = this;
  $(function() {
    $.ajax({type: "GET", url: me.gadgetUrl, dataType: "xml", timeout: 5000,
      error: function() {
        me.gadgetContentDiv.innerHTML
            = 'Cannot fetch gadget info from ' + me.gadgetUrl;
      },
      success: function(gadgetXml) {
        me.onLoadGadget(gadgetXml);
      }
    }); // ajax request
  }); // jquery function call
};


/**
 * This function will get called after successful download of the gadget XML.
 * @private
 */
Sandbox.prototype.onLoadGadget = function(xmlGadget) {
  // will get displayed only on error
  var content = 'No valid html content in gadget definition.';
  // place holder name in case there is no title
  var title = 'Gadget';

  $(xmlGadget).find('ModulePrefs').each(function() {
    title = $(this).attr('title');
  });
  $(xmlGadget).find('Content').each(function() {
    content = $(this).text();
  });

  // Build up the title area text and then the gadget content itself
  var htmlText = title;
  htmlText += '<br>Gadget Location: <a href="' + this.gadgetUrl + '">'
      + this.gadgetUrl + '</a>';
  htmlText += '<br>Container State: <a href="' + this.stateUrl + '">'
      + this.stateUrl + '</a>';
  htmlText += '<br><hr>' + content;

  $('#' + this.gadgetContentDivId).html(htmlText);
};


/**
 * Simulate _IG_RegisterOnloadHandler since many gadgets use it
 */
function _IG_RegisterOnloadHandler(callback) {
  window.setTimeout(function(){
    callback();
  }, 0);
};


/**
 * Simulate _IG_AdjustIFrameHeight since many gadgets use it
 */
function _IG_AdjustIFrameHeight() {
  // no-op for this container
};


/**
 * Init the sandbox
 */
var sandbox = new Sandbox();
sandbox.init();
